﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;

using KnowledgeBase;
using KnowledgeBaseIntermediary;
using InputHandler;
using ActionReporter;
using OutputHandler;
using LisaConfigurator;

namespace LisaCore
{
    /// <summary>
    /// Lisa's Core. This component performs the logical state changes that allow Lisa to make "intelligent"
    /// decisions. It contains the entire Finite State Machine and uses a PriorityQueue to temporarily store states.
    /// </summary>
    public class CoreUnit:ICore
    {
        //Private variables
        Thread mainThread;  //Core's main thread
        Boolean running;    //true if Core is running, false otherwise.
        RUNMODE runMode;
        
        //Component declarations
        IInputHandler inputHandler;
        IActionReporter actionReporter; //Used to report Lisa's actions back to listening components (Great for debugging and testing)
        IKnowledgeBase knowledgeBase;
        IOutputHandler outputHandler;   //The output handler
        IConfigurationAccessor lisaConfiguration;   //Lisa's configuration

        //State machine related declarations
        StateDTO currentState;  //The state Lisa is currently in. If it's null, Lisa is in standby.
        bool autoCommandsCompleted; //True if the automatic commands of the current state have all been executed
        bool userRequestedSwitchToStandby;    //True if user asked Lisa to return to standby state
        IPriorityQueue<StateDTO> stateQueue;    //The priority queue that holds all the unvisited states
        bool waitingForUserPermission;  //True if Lisa is currently waiting for the user's permission
        bool userPermissionReceived;    //True if Lisa received user's permission
        bool userPermissionDenied;      //True if user denied Lisa permission to switch states
        StateDTO unconfirmedNewState;   //The state to which Lisa will switch after receiving the user's explicit permission. If she doesn't receive permission within the timeout period or receives a denial to switch, she ignores this state
        const int PERMISSION_TIMEOUT = 10;   //Maximum amount of time Lisa should wait for the user's permission to switch states before ignoring the new state
        System.Timers.Timer manualCommandTimer;    //Keeps track of how long Lisa should wait for the user to give her manual commands
        int manualCommandWaitTimeElapsed;   //The amount of time Lisa has been waiting for a manual command from the user
        const int MANUAL_COMMAND_TIMEOUT = 30;  //Maximum amount of time Lisa should wait between manual commands

        //Multithreading-related declarations
        object lockObject;  //used to synchronize threads
        object inputHandlerLockObject;   //used to synchronize threads from input handler

        #region Accessors
        public RUNMODE RunMode
        {
            get { return runMode; }
            set { runMode = value; }
        }

        public IActionReporter ActionReporter
        {
            get { return actionReporter; }
            set { actionReporter = value; }
        }
        #endregion

        #region ICore Interface methods
        /// <summary>
        /// Initializes the Core Unit.
        /// </summary>
        public void initialize()
        {
            Console.WriteLine("Initializing Lisa...");
            mainThread = new Thread(run);
            running = false;
            lisaConfiguration = new ConfiguratorImpl();
            knowledgeBase = new KnowledgeBaseImpl();
            inputHandler = new InputHandlerImpl();
            inputHandler.initialize(knowledgeBase,lisaConfiguration);
            inputHandler.commandReceived += new HandleInput(inputHandler_commandReceived);
            inputHandler.environmentEventReceived += new HandleInput(inputHandler_environmentEventReceived);
            outputHandler = new OutputHandler_Impl();
            runMode = RUNMODE.DEBUG;
            currentState = null;
            autoCommandsCompleted = false;
            userRequestedSwitchToStandby = false;
            stateQueue = new PriorityQueueImpl<StateDTO>(10);
            waitingForUserPermission = false;
            userPermissionReceived = false;
            userPermissionDenied = false;
            manualCommandTimer = new System.Timers.Timer();
            manualCommandTimer.Interval = 1000;
            manualCommandTimer.Elapsed += new System.Timers.ElapsedEventHandler(manualCommandTimer_Elapsed);
            manualCommandWaitTimeElapsed = 0;
            lockObject = new object();
            inputHandlerLockObject = new object();
        }

        /// <summary>
        /// Starts the Core.
        /// </summary>
        public void start()
        {
            Console.WriteLine("Starting Lisa...");
            mainThread.Start();
            running = true;
            inputHandler.start();
        }

        /// <summary>
        /// Stops the Core.
        /// </summary>
        public void stop()
        {
            Console.WriteLine("Stopping Lisa...");
            running = false;
            inputHandler.stop();
        }

        /// <summary>
        /// Returns an InputHandler for debugging purposes if Lisa is in DEBUG mode. This is useful if a GUI starts Lisa and wants to access
        /// its InputHandler for debugging purposes.
        /// </summary>
        /// <returns></returns>
        public IInputHandlerDebug getInputHandler()
        {
            if (runMode == RUNMODE.DEBUG)
            {
                return (IInputHandlerDebug)inputHandler;
            }
            else
            {
                throw new Exception("Access Denied: InputHandler can only be debugged when Lisa is in DEBUG mode.");
            }
        }
        #endregion

        #region Private helper methods
        void run()
        {
            while (running)
            {
                if (!waitingForUserPermission)
                {
                    if (currentState == null)  //Lisa is in standby mode
                    {
                        runStandbyState();
                    }
                    else    //Lisa is in an active state
                    {
                        runActiveState();
                    }
                }
                //[ME] Added sleep. I guess this is the problem. (dont kill me for editing your code. please?)
                Thread.Sleep(20);
            }
        }

        void runStandbyState()
        {
            if (!stateQueue.isEmpty()) //If state priority queue is not empty
            {
                StateDTO newPotentialState = stateQueue.Dequeue();
                if (newPotentialState.IsChecked) //New potential state is checked
                {
                    //Ask user for permission to switch before switching to new state
                    unconfirmedNewState = newPotentialState;
                    waitForUserPermission(newPotentialState);
                }
                else
                {
                    //Switch to new state
                    attemptStateChange(newPotentialState);
                }
            }
        }

        void runActiveState()
        {
            //Execute all the automatic commands if there are any and if they haven't been executed already
            if (!autoCommandsCompleted)
            {
                foreach (String autoCommand in currentState.getAutoCommands())
                {
                    if (runMode == RUNMODE.DEBUG) //If in Debug mode, report actions to ActionReporter
                    {
                        actionReporter.reportCommandExecution("[Auto] " + autoCommand);
                    }
                    CommandObject commandObj = knowledgeBase.getUDPByCommand(currentState.Name, autoCommand);
                    outputHandler.sendCommand(commandObj);
                }
                autoCommandsCompleted = true;
            }
            else if (currentState.getManualCommands().Count > 0 ||
                currentState.getScenes().Count>0) //If current state has any manual commands or scenes that can be executed
            {
                //Wait for any manual commands from user. If user doesn't state any manual commands for a
                //given amount of time (Time Out), switch back to the standby state.
                manualCommandTimer.Start();
                while (manualCommandWaitTimeElapsed < MANUAL_COMMAND_TIMEOUT && 
                    !userRequestedSwitchToStandby) //Check if time has elapsed or if Lisa has just switched to a new state (standby or another active state)
                {
                    //[ME] Added sleep. I guess this is the problem. (dont kill me for editing your code. please?)
                    Thread.Sleep(20);
                    System.Diagnostics.Debug.WriteLine("Manual timer: " + manualCommandWaitTimeElapsed);
                }
                //Reset values
                manualCommandTimer.Stop();
                manualCommandWaitTimeElapsed = 0;
                
                //Switch back to standby state
                if (userRequestedSwitchToStandby) //If user told lisa to switch to standby
                {
                    //Output that lisa switched to standby due to user command
                    outputHandler.spill(new OutputObject("Switching to Stbdy due to user command", lisaConfiguration.getReturnToStandbyOnCommandTTS()));
                }
                else //If Lisa is switching to standby due to timeout
                {
                    outputHandler.spill(new OutputObject("Switching to Stdby due to time out", lisaConfiguration.getReturnToStandbyOnTimeoutTTS()));
                } 
                attemptStateChange(null);
                userRequestedSwitchToStandby = false;
            }
            else
            {
                //Switch back to standby state.
                outputHandler.spill(new OutputObject("Switching to Stdby due to time out", lisaConfiguration.getReturnToStandbyOnTimeoutTTS()));
                attemptStateChange(null);
            }
        }

        /// <summary>
        /// Asks the user for permission before switching to new state. While waiting for the user's permission,
        /// Lisa is blocked. If the user doesn't give permission to switch after a given period of time (Timeout),
        /// Lisa ignores the new state. If the user denies permission, Lisa ignores the new state. If the user
        /// authorizes the state-change, Lisa switches to the new state.
        /// </summary>
        /// <param name="state">The new potential state to which Lisa should switch.</param>
        void waitForUserPermission(StateDTO newState)
        { 
            //Wait for user's permission before switching to new state.
            waitingForUserPermission = true;
            StateDTO state = (StateDTO)newState;
            if (runMode == RUNMODE.DEBUG)
            {
                //Report requirement of user permission to action reporter
                if (actionReporter != null)
                {
                    actionReporter.reportUserPermissionRequired("Switch to " + state.Name + " state?");
                }
            } 
            //Output
            outputHandler.spill(new OutputObject("Waiting for permision", lisaConfiguration.getUserPermissionRequiredTTS(state.Name)));
            DateTime permissionStartTime = DateTime.Now;
            DateTime currentTime = DateTime.Now;
            TimeSpan duration = currentTime-permissionStartTime;
            int userPermissionTimeElapsed = duration.Hours + duration.Minutes + duration.Seconds;
            while (userPermissionTimeElapsed < PERMISSION_TIMEOUT && waitingForUserPermission)
            {
                //Wait
                currentTime = DateTime.Now;
                duration = currentTime - permissionStartTime;
                userPermissionTimeElapsed = duration.Hours + duration.Minutes + duration.Seconds;
                
                //[ME] Added sleep. I guess this is the problem. (dont kill me for editing your code. please?)
                Thread.Sleep(20);
            }
            //Done waiting
            if (runMode == RUNMODE.DEBUG)
            {
                //Report termination of permission wait period to action reporter
                if (actionReporter != null)
                {
                    actionReporter.reportSystemInfo("Done waiting for permission");
                }
            }
            if (userPermissionReceived)
            {
                attemptStateChange(newState);
            }
            else if(!userPermissionDenied) //Permission timed out
            {
                outputHandler.spill(new OutputObject("Permission timeout", lisaConfiguration.getPermissionTimeoutTTS()));
            }
            waitingForUserPermission = false;
            userPermissionReceived = false;
            userPermissionDenied = false;
        }

        /// <summary>
        /// Switches to the new state and handles any and all the cleanup work from the old state.
        /// </summary>
        /// <param name="newState">The new state to which to switch.</param>
        void switchState(StateDTO newState)
        {
            if (runMode == RUNMODE.DEBUG) //If Lisa is in debug mode
            {
                //Report actions to action reporter
                if (actionReporter != null)
                {
                    String currentStateName, newStateName;
                    if (currentState == null)
                        currentStateName = "Standby State";
                    else
                        currentStateName = currentState.Name;
                    if (newState == null)
                        newStateName = "Standby State";
                    else
                        newStateName = newState.Name;
                    actionReporter.reportStateChange(currentStateName, newStateName);
                }
            }
            //Output some spoken TTS text        
            if (newState != null)
                outputHandler.spill(new OutputObject("Switching states", newState.OutputObject.getTtsText()));

            //Inform InputHandler of state change
            inputHandler.changeState(newState);

            //Reset values
            manualCommandTimer.Stop();
            manualCommandWaitTimeElapsed = 0;
            waitingForUserPermission = false;
            userPermissionReceived = false;
            userPermissionDenied = false;
            userRequestedSwitchToStandby = false;

            currentState = newState;

            //Init values of new state
            //If new state has any automatic commands that need to be executed, autoCommandsCompleted is set to false so
            //that auto commands of new state are executed
            if (newState != null)
            {
                autoCommandsCompleted = newState.getAutoCommands().Count == 0;
            }
            else //Switching to stand by state
                autoCommandsCompleted = false;
        }

        /// <summary>
        /// Attempts to switch to the new state given the circumstances.
        /// </summary>
        /// <param name="newState">The new state which Lisa should attempt to switch to.</param>
        void attemptStateChange(StateDTO newState)
        {
            lock (this)
            {
                if (newState == null) //Attempting to switch to standby state
                {
                    if (autoCommandsCompleted && !waitingForUserPermission) //If automatic commands are completed and not waiting for user's permission
                    {
                        //Switch immediately
                        switchState(null);
                    }
                }
                else //Attempting to switch to an active state
                {
                    if (currentState == null) //If Lisa is currently in standby state
                    {
                        if (newState.IsChecked) //If new state is checked
                        {
                            if (userPermissionReceived) //If user permission received
                            {
                                switchState(newState);
                            }
                        }
                        else //New state is not checked
                        {
                            switchState(newState); //Switch states
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Evaluates and processes a new state. This basically consists of enqueing the state and outputting
        /// an appropriate message.
        /// </summary>
        /// <param name="newState">The new state which should be evaluated and processed</param>
        void evaluateAndProcessNewState(StateDTO newState)
        {
            if (newState != null) //If new state is an active state
            {
                if(currentState!=null) //If Lisa is currently in an active state
                {
                    if (newState.Priority > currentState.Priority) //New state has higher priority than current state
                    {
                        //Output message stating that new state is more important than current active state
                        outputHandler.spill(new OutputObject("Busy but new higher prio. state", lisaConfiguration.getCurrentlyBusyButNewStateHigherPriorityTTS()));
                    }
                    else
                    {
                        //Output message stating that new state can't be processed at the moment
                        outputHandler.spill(new OutputObject("Currently busy", lisaConfiguration.getCurrentlyBusyTTS()));
                    }
                }
                //Enqueue state
                stateQueue.Enqueue(newState, (int)newState.Priority);
            }
        }

        /// <summary>
        /// Evaluates and processes a new user command.
        /// </summary>
        /// <param name="command">The command to be evaluated and processed.</param>
        void evaluateAndProcessNewCommand(String command)
        {
            if (waitingForUserPermission) //If Lisa is waiting for the user's permission
            {
                if (lisaConfiguration.getGrantPermissionCommands().Contains(command)) //Lisa receives user permission
                {
                    outputHandler.spill(new OutputObject("Permisison Confirmation", lisaConfiguration.getPermissionConfirmedTTS()));
                    waitingForUserPermission = false;
                    userPermissionReceived = true;
                }
                else if (lisaConfiguration.getDenyPermissionCommands().Contains(command)) //User denies Lisa permission
                {
                    outputHandler.spill(new OutputObject("Permisison Denial", lisaConfiguration.getPermissionDeniedTTS()));
                    waitingForUserPermission = false;
                    userPermissionDenied = true;
                }
            }
            else //If Lisa is not waiting for the user's permission
            {
                if (command.StartsWith(lisaConfiguration.getStateChangePrefix()))  //If command can cause a state change
                {
                    //Resolve command to a state dto
                    command = command.Replace(lisaConfiguration.getStateChangePrefix(), "");   //Remove the state-change prefix
                    evaluateAndProcessNewState(knowledgeBase.getDTOByCommand(command));  //resolve command and process resulting state
                }
                else if (lisaConfiguration.getReturnToStandbyCommands().Contains(command)) //If Lisa receives the "Thank you" command
                {
                    if (autoCommandsCompleted)
                    {
                        userRequestedSwitchToStandby = true;
                    }
                }
                else if (command.StartsWith(lisaConfiguration.getScenePrefix())) //Received a scene command
                {
                    if (currentState != null)
                    {
                        if (autoCommandsCompleted)
                        {
                            //Resolve command to a scene
                            //TODO: Obtain Scene TTS. Requires change in KnowledgeBase
                            command = command.Replace(lisaConfiguration.getScenePrefix(), "");
                            List<String> sceneCommands = knowledgeBase.getSceneCommands(currentState.Name, command);
                            String sceneCommand;
                            int i = 0;
                            while (currentState != null && i < sceneCommands.Count)
                            {
                                sceneCommand = sceneCommands[i];
                                CommandObject commandObj = knowledgeBase.getUDPByCommand(currentState.Name, sceneCommand);
                                outputHandler.sendCommand(commandObj);
                                if (runMode == RUNMODE.DEBUG) //If in Debug mode, report actions to ActionReporter
                                {
                                    actionReporter.reportCommandExecution("[Scene Command] " + sceneCommand);
                                }
                                //reset manual command wait time
                                manualCommandWaitTimeElapsed = 0;
                                i++;
                            }
                        }
                    }
                }
                else //Lisa receives an in-state command such as "Volume Up"
                {
                    if (currentState != null)
                    {
                        if (autoCommandsCompleted)
                        {
                            CommandObject commandObj = knowledgeBase.getUDPByCommand(currentState.Name, command);
                            if (runMode == RUNMODE.DEBUG) //If in Debug mode, report actions to ActionReporter
                            {
                                actionReporter.reportCommandExecution("[Manual] " + command);
                            }
                            //Output
                            outputHandler.sendCommand(commandObj);
                            //reset manual command wait time
                            manualCommandWaitTimeElapsed = 0;
                        }
                    }
                }
            }
        }

        #endregion

        #region Listener methods for Timers

        void userPermissionTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
        {
        }

        void manualCommandTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
        {
            manualCommandWaitTimeElapsed++;
        }

        #endregion

        #region Event-related methods
        void inputHandler_commandReceived(InputEventArgs args)
        {
            Console.WriteLine("> " + args.Command + " received.");
            String command = args.Command;
            if (runMode == RUNMODE.DEBUG) //If in Debug mode, report actions to ActionReporter
            {
                actionReporter.reportSystemInfo(">Rec. Command " + command);
            }
            evaluateAndProcessNewCommand(command);
        }

        void inputHandler_environmentEventReceived(InputEventArgs args)
        {
            if (runMode == RUNMODE.DEBUG) //If in Debug mode, report actions to ActionReporter
            {
                actionReporter.reportSystemInfo(">Rec. Env. Event: " + args.Command);
            }
            outputHandler.spill(new OutputObject("Environment Event received", lisaConfiguration.getEnvironmentEventReceivedTTS()));
            evaluateAndProcessNewState(knowledgeBase.getDTOByEvent(args.Command));
        }
        #endregion

        #region The following methods only perform their functions when Lisa is in Debug mode

        /// <summary>
        /// This method simulates a potential state change. The new state to which Lisa could potentially switch
        /// is passed in the parameters. This method only works if Lisa's in Debug Mode.
        /// </summary>
        /// <param name="newState">The new state to which Lisa could potentially switch.</param>
        public void createPotentialStateChange(StateDTO newState)
        {
            evaluateAndProcessNewState(newState);
        }

        /// <summary>
        /// This method simulates the receipt of a user command. This includes the receipt of user-permissions etc.
        /// </summary>
        /// <param name="command">The in-state command or scene.</param>
        public void simulateCommandReceipt(String command)
        {
            if (runMode == RUNMODE.DEBUG)
            {
                if (actionReporter != null)
                {
                    actionReporter.reportCommandExecution("[Manual] " + command);
                }
            }
            if (waitingForUserPermission) //If Lisa is waiting for the user's permission
            {
                if(lisaConfiguration.getGrantPermissionCommands().Contains(command))
                {
                    waitingForUserPermission = false;
                }
                else if (lisaConfiguration.getDenyPermissionCommands().Contains(command))
                {
                    waitingForUserPermission = false;
                }
            }
        }

        #endregion
    }
}
